import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.LinkedList;

/** Bridge Class
    CS158A Spring 2010 Project #2
    @author Clare Lanam & Loc Phan
    */

public class Bridge implements Runnable
{
   /** bridge ID */
   private final String ID;
   /** whether or not this bridge thinks it is the tree's root */
   private boolean isRoot;
   /** stored root bridge ID */
   private String rootBridgeID;
   /** ports on this bridge */
   private LinkedList<Port> ports;
   /** root port */
   private Port rootPort;
   /** time since last update from root */
   private Calendar timeOfLastRootUpdate;
   
   // ------[DISABLED FLAG]------ //
   
   /** use to manually disable or enable this bridge */
   public boolean DISABLED = false;
   
   // ------[TIMERS]------ //
   
   /** delay between BDPU outputs (in milliseconds)
       default is 2 seconds (min 1s, max 10s) */
   public static final int HELLO_TIME = 2000;
   /** number of milliseconds before a switch discards a BDPU's info */
   public static final int MAX_MESSAGE_AGE = 15000;
   /** length of learning stage (and of the listening stage in stp)
       default 20s (min 6s, max 40s) */
   public static final int FORWARD_DELAY = 20000;
   
   // ------[CONSTRUCTOR]------ //
   
   public Bridge(String ID)
   {
      this.ID = ID;
      ports = new LinkedList<Port>();
      timeOfLastRootUpdate = new GregorianCalendar();
      setSelfAsRootBridge();
   }
   
   // ------[ACCESSORS]------ //
   
   public String getID() { return ID; }
   public int getPathCostToRoot() { if (rootPort == null) return 0;
      return rootPort.getPathCost(); }
   public LinkedList<Port> getPorts() { return ports; }
   public String getStoredRootBridgeID() { return rootBridgeID; }
   public boolean isRoot() { return isRoot; }
   public String toString() { return ID; }
   
   // ------[THREAD RUN AREA]------ //
   
   /** Simulates the RSTP algorithm. */
   
   public void run()
   {
      try 
      {
         while (true)
         {
            if (DISABLED)
               continue;
            
            checkPortsAreStillReceiving();
            
            if (isRoot)
               timeOfLastRootUpdate = new GregorianCalendar();

            sendBPDUs();            
            Thread.sleep(HELLO_TIME);
         }
      }
      catch (InterruptedException exception) { ; }
   }
   
   // ------[PUBLIC METHODS]------ //
   
   /** Returns whether this bridge has a direct connection to
       Bridge b. */
   
   public synchronized boolean isLinkedDirectlyTo(Bridge b)
   {
      if (b == null)
         return false;
      
      for (Port p : ports)
         if (p.getDistalBridge() == b)
            return true;
      
      return false;
   }
   
   /** Creates a new link between two bridges. */
   
   public static synchronized void link(Bridge a, Bridge b, int linkCost)
   {
      Link link = new Link(linkCost);
      Port pA = new Port(a);
      Port pB = new Port(b);
      link.attach(pA);
      link.attach(pB);
      a.ports.add(pA);
      b.ports.add(pB);
   }
   
   /** Returns true if this bridge matches a given bridge ID.
       @return true if this bridge matches a given bridge ID
       */
   
   public boolean matches(String testID)
   {
      return ID.matches(testID);
   }
   
   /** Handles reception of a BPDU.
       @param source the port that received the BPDU
       @param bpdu the BPDU received
       */
   
   public synchronized void receive(Port source, BPDU bpdu)
   {
      if (DISABLED) // manually disabled state :)
         return;
      
      if (bpdu.ROOT.compareTo(rootBridgeID) > 0) // bad BPDU
      {
         source.setRole(PortRole.DESIGNATED); // activate this side
         if (source == rootPort) // if this is root port, need new one
            this.designateRootPort(); // go fetch, little code! :)
         
         source.send(generateBPDU(), true); // send reply back
      }
      
      if (bpdu.LAST_ROOT_UPDATE.getTimeInMillis() <
            (new GregorianCalendar()).getTimeInMillis() - MAX_MESSAGE_AGE)
         return; //  too old
      
      // if BPDU has better info, update self:
      
      if (bpdu.ROOT.compareTo(rootBridgeID) < 0) // found better root
      {
         rootBridgeID = bpdu.ROOT;
         isRoot = (ID.equals(rootBridgeID));
         for (Port p : ports)
            p.resetToDesignated();
         setRootPort(source);
         source.setPathCost(bpdu.PATH_COST + source.getLinkCost());
         return;
      }
      
      if (isRoot || !bpdu.ROOT.equals(rootBridgeID))
         return;
      
      timeOfLastRootUpdate = bpdu.LAST_ROOT_UPDATE;
      
      // ------:update & check path costs:------
      
      int pCost = bpdu.PATH_COST + source.getLinkCost();
      source.setPathCost(pCost); // update path cost
      if (pCost < getPathCostToRoot() || rootPort == null)
         setRootPort(source); // found new root port
      else if (pCost == getPathCostToRoot() &&
            (source.getDistalBridge().ID.compareTo(
                  rootPort.getDistalBridge().ID) < 0))
         setRootPort(source); // found new root port at lower ID

      if (source != rootPort) // determine port role
      {
         Port bestOnLink = source.getLink().getLowestCostPort(); 
         if (source == bestOnLink)
            source.setRole(PortRole.DESIGNATED);
         else if (source.getBridge() == bestOnLink.getBridge())
            source.setRole(PortRole.BACKUP);
         else
            source.setRole(PortRole.ALTERNATE);
      }
   }
   
   /** Unlinks all connections between two bridges. */
   
   public static synchronized void unlink(Bridge a, Bridge b)
   {
      Iterator<Port> iter = a.ports.iterator();
      
      while (iter.hasNext())
      {
         Port p = iter.next();
         
         if (p.getDistalBridge() == b)
         {
            Port distal = p.getDistalPort();
            b.ports.remove(distal);
            p.getLink().removeAllPorts();
            iter.remove();
            
            if (p == a.rootPort) a.designateRootPort();
            if (distal == b.rootPort) b.designateRootPort();
         }
      }
   }
   
   // ------[PRIVATE HELPER METHODS]------ //
   
   /** Make sure ports are still receiving BPDUs. */
   
   private synchronized void checkPortsAreStillReceiving()
   {
      for (Port p : ports)
      {
         if ((p.getRole() != PortRole.DESIGNATED)
               && (p.getTimeLastBPDUReceived() < 
                  (new GregorianCalendar()).getTimeInMillis() - 3*HELLO_TIME))
         {
            // time out ports after 3 hello times
            p.resetToDesignated();
            if (p == rootPort)
               setSelfAsRootBridge();
         }
      }
   }
   
   /** Figures out which port, if any, should be root */
   
   private synchronized void designateRootPort()
   {
      Port tempRoot = null;
      
      for (Port p : ports)
      {
         if (p.getRole() == PortRole.ROOT) // only want one root port
            p.setState(PortState.DISCARDING);
         
         if (p.getRole() == PortRole.ALTERNATE)
         {
            if (tempRoot == null)
               tempRoot = p;
            else if (p.getPathCost() < tempRoot.getPathCost())
               tempRoot = p;
            else if ((p.getPathCost() == tempRoot.getPathCost())
                  && (p.getBridge().ID.compareTo(tempRoot.getBridge().ID) < 0))
               tempRoot = p;
         }
      }
      
      if (tempRoot != null)
         setRootPort(tempRoot);
      else setSelfAsRootBridge();
   }
   
   /** Generates a BPDU with current info. */
   
   private BPDU generateBPDU()
   {
      if (isRoot)
         return new BPDU(ID, rootBridgeID,
               getPathCostToRoot(), new GregorianCalendar());
      return new BPDU(ID, rootBridgeID,
               getPathCostToRoot(), timeOfLastRootUpdate);
   }
   
   /** Sends out BPDUs. */
   
   private void sendBPDUs()
   {
      for (Port p : ports) // p will only obey if role is designated
         p.send(generateBPDU());
   }
   
   /** Sets a port as root port. */
   
   private void setRootPort(Port p)
   {
      if (p != null && p != rootPort) // found new & better root port! :D
      {
         for (Port koala : ports)
         {
            // temporarily disable all other links
            koala.setRole(PortRole.BACKUP);
            koala.setState(PortState.LEARNING);
         }
         
         p.setRole(PortRole.ROOT);
      }
      else if (p == null && rootPort != null)
         rootPort.setState(PortState.DISCARDING);
      
      rootPort = p;
   }
   
   /** Sets this bridge's status to root. */
   
   private synchronized void setSelfAsRootBridge()
   {
      isRoot = true;
      rootBridgeID = ID;
      rootPort = null;
      timeOfLastRootUpdate = new GregorianCalendar();
      
      for (Port p : ports)
         p.resetToDesignated();
   }
}
